<?php
/**
 * @author Artur Bier <izatop@gmail.com>
 * @package Db.Query
 *
 * @copyright TinyWEB CMS
 * @link http://tinywebcms.ru
 * @link http://cms.vingrad.ru
 * @version SVN: $Id$ 
 */
 

/**
 * class DbQueryConstructor
 *
 */
class DbQueryConstructor extends Object
{
    /**
     * Контекст Model.
     *
     * @var Model
     */
    private $context;
    
    /**
     * Фильтры.
     *
     * @var array
     */
    protected $filters          = array();
    
    /**
     * Поля результата.
     *
     * @var array
     */
    protected $select           = array('*');
    
    /**
     * Класс-родитель конструктор запросов, к которому 
     * присоединен текущий экземпляр класса (контекст).
     *
     * @var DbQueryConstructor
     */
    private $parent             = null;
    
    /**
     * Присоединенные к запросу таблицы.
     *
     * @var array
     */
    protected $join             = array();
    
    /**
     * Имя таблицы.
     *
     * @var string
     */
    protected $table;
    
    protected $alias;
    
    /**
     * Готовый SQL запрос.
     *
     * @var unknown_type
     */
    protected $sql;
    
    protected $order            = array();
    protected $group            = array();
    
    protected $useIndex;
    
    /**
     * Лимит записей.
     *
     * @var array
     */
    protected $limit            = array();
    
    protected $binds            = array();
    
    const INNER_JOIN            = 1;
    const LEFT_JOIN             = 2;
    const OUTER_JOIN            = 3;
    const RIGHT_JOIN            = 4;
    
    /**
     * Конструктор
     *
     * @param DataModel $context
     */
    public function __construct(Model $context)
    {
        $this->context  = $context;
        $this->table    = $context->getTableName();
        $this->alias    = $this->table;
    }
    
    /**
     * Сборка SQL запроса.
     * @return string
     */
    protected function assembling()
    {
        $this->alias = ($this->alias) ? $this->alias : $this->table;
        $this->sql  = "SELECT " . $this->_sqlOfSelect() . " FROM `{$this->table}` as `{$this->alias}`  " . $this->_sqlOfTableDefinition() . 
            $this->_sqlOfJoin() . " WHERE " . $this->_sqlOfWhere() . " " . $this->_sqlOfWhereBindings() . " " . $this->_sqlOfGroup() .
            " " . $this->_sqlOfOrder() . " " . $this->_sqlOfLimit();

        return $this->sql;
    }
    
    protected function assemblingCount()
    {
        $this->sql  = "SELECT COUNT(*) as count FROM `{$this->table}` as `{$this->alias}` ".
             $this->_sqlOfJoin() . " WHERE " . $this->_sqlOfWhere() . " " . $this->_sqlOfWhereBindings();

        return $this->sql;
    }
    
    /**
     * Указывает какие поля будут присутствовать в результате.
     * Внимение! Метод ЗАМЕНЯЕТ, не добавляет поля.
     *
     * @param array $fields
     * @return DbQueryConstructor
     */
    public function fillSelect($fields)
    {
        $this->select   = (array) $fields;
        return $this;
    }
    
    /**
     * Указывает какие поля будут присутствовать в результате.
     * Внимение! Метод ЗАМЕНЯЕТ, не добавляет поля.
     *
     * @param array $fields
     * @return DbQueryConstructor
     */
    public function fields($fields)
    {
        $this->select   = (array) $fields;
        return $this;    
    }
    
    public function getSQL()
    {
        return $this->assembling();
    }
    
    /**
     * Возвращает имя таблицы связанной с конструктором.
     *
     * @return string
     */
    public function getTableName()
    {
        return $this->table;
    }
    
    public function getTableAlias()
    {
        return $this->alias;
    }
    
    public function _sqlOfTableDefinition()
    {
        if($this->useIndex) {
            return " USE INDEX(" . $this->useIndex . ") ";
        }
        
        return '';
    }
    
    /**
     * Возвращает SQL для конструкции SELECT.
     *
     * @return string
     */
    public function _sqlOfSelect()
    {
        $sql    = '';
        if(sizeof($this->select))
        {
            foreach ($this->select as $field) {
                $sql    .= (mb_strpos($field, "(") === false ? "`{$this->alias}`." : '') . $field . ", ";
            }
        }
        
        if(sizeof($this->join) && !$this->hasParent())
        {
            foreach ($this->join as &$join)
            {
                list($ref, $type)   = $join;
                $_join_select       = $ref->_sqlOfSelect();
                if($_join_select != '') {
                    $sql .= $_join_select . ", ";
                }
            }
        }
        
        return rtrim($sql, ", ");
    }
    
    public function _sqlOfWhereBindings()
    {
        $sql = "";
        foreach ($this->binds as $bind)
        {
            list($ref, $refield, $field, $operator)  = $bind;
            
            switch ($operator)
            {
                case '=':
                case '!=':
                case '<>':
                case '>=':
                case '>':
                case '<=':
                case '<':
                    $sql    .= " AND `{$this->alias}`.`{$field}` {$operator} `" . $ref->getTableAlias() . "`.`{$refield}`";
                    break;
                default:
                    throw new Exception('Assembling binds failed. I don\'t know SQL operator "'.$operator.'".');
            }
        }
        
        return $sql;
    }
    
    /**
     * Возвращает SQL для конструкции WHERE.
     *
     * @return string
     */
    public function _sqlOfWhere()
    {
        $sql    = "1";
        foreach ($this->filters as $filter)
        {
            list($field, $data, $operator)  = $filter;

            if(is_array($data)) {
	        	foreach ($data as &$value) { 
	        	    if(!$value instanceof DbCondition) {
	            	    $value = $this->context->escape($value);
	        	    } 
	           	}
            } else if(!$data instanceof DbCondition) {
            	$data = $this->context->escape($data);
            }
           	
            switch (strtoupper($operator))
            {
                case 'IN':
                	if(is_array($field)) {
                		$sql .= " AND (";
                		foreach($field as $f) {
                			$sql    .= "`{$this->alias}`.`{$f}` IN('" . implode("','", $data) . "') OR";
                		}
                		$sql = rtrim($sql, 'OR ') . ") ";
                	} else {
                    	$sql    .= " AND `{$this->alias}`.`{$field}` IN('" . implode("','", $data) . "')";
	               	}
	               	
                    break;
                    
                case 'NOT IN':
            		if(is_array($field)) {
                		$sql .= " AND (";
                		foreach($field as $f) {
                			$sql    .= "`{$this->alias}`.`{$f}` NOT IN('" . implode("','", $data) . "') OR";
                		}
                		$sql = rtrim($sql, 'OR ') . ") ";
                	} else {
                    	$sql    .= " AND `{$this->alias}`.`{$field}` NOT IN('" . implode("','", $data) . "')";
	               	}
	               	
                    break;
                    
                case 'BETWEEN':
                	if(is_array($field)) {
                		throw new Exception('Db: invalid value for BETWEEN.');
                	}
                	
                    $sql .= " AND (`{$this->alias}`.`{$field}` BETWEEN ";
                    $sql .= ($data[0] === null ? 'NULL' : (!$data[0] instanceof DbCondition ? '"' . $data[0] . '"' : (string) $data[0]));
                    $sql .= " AND " . ($data[1] === null ? 'NULL' : (!$data[1] instanceof DbCondition ? '"' . $data[1] . '"' : (string) $data[1])) . ")";
                    break;

                case '=':
                case '!=':
                case '>':
                case '>=':
                case '<':
                case '<=':
                case '<>':
                case 'IS':
                case 'IS NOT':
                case 'LIKE':
                	if(is_array($field)) {
                		$sql .= " AND (";
                		foreach ($field as $f) {
                			$value  = ($data === null) ? 'NULL' : ((!$data instanceof DbCondition) ? '"' . $data . '"' : (string) $data);
                    		$sql    .= "`{$this->alias}`.`{$f}` {$operator} " . $value . " OR";
                		}
                		$sql = rtrim($sql, 'OR ') . ") ";
                	} else {
                    	$value  = ($data === null) ? 'NULL' : ((!$data instanceof DbCondition) ? '"' . $data . '"' : (string) $data);
                    	$sql    .= " AND `{$this->alias}`.`{$field}` {$operator} " . $value;
                	}
                    break;
                
                case 'IS NULL':
                    $sql    .= " AND `{$this->alias}`.`{$field}` IS NULL ";
                    break;
                    
                default:
                    throw new Exception('Assembling filters failed. I don\'t know SQL operator "'.$operator.'".');
            }
        }
        
        return $sql;
    }
    
    /**
     * Возвращает SQL для конструкции JOIN.
     *
     * @return string
     */
    public function _sqlOfJoin()
    {
        $sql    = '';
        foreach ($this->join as &$join) 
        {
            list($ref, $type)   = $join;
            
            if($ref->getTableName() == $this->getTableName())
            {
                throw new Exception('Assembling join SQL failed. I\'m not be suicide!');
            }
            
            switch ($type)
            {
                case self::INNER_JOIN : $type = 'INNER JOIN'; break;
                case self::LEFT_JOIN : $type = 'LEFT JOIN'; break;
                case self::OUTER_JOIN : $type = 'OUTER JOIN'; break;
                case self::RIGHT_JOIN : $type = 'RIGHT JOIN'; break;
                default: $type = 'INNER JOIN';
            }
            
            $_join_on   = $ref->_sqlOfWhere();
            $_join_on   .= " " . $ref->_sqlOfWhereBindings();
            if($_join_on == "1")
            {
                throw new Exception('Assembling join SQL failed. Empty condition in JOIN by "' . $ref->getTableName() . '"');
            }
            
            $sql    .= "{$type} `" . $ref->getTableName() . "` `" . $ref->getTableAlias() . "` ON " . $_join_on . " ";
        }
        
        return $sql;
    }
    
    /**
     * Возвращает SQL для конструкции ORDER.
     *
     * @return string
     */
    public function _sqlOfOrder()
    {
        $sql = '';
        
        if(count($this->order))
        {
            $sql    = 'ORDER BY';
            foreach ($this->order as $field => $direct)
            {
                $sql .= " `$this->alias`.`$field` $direct, ";
            }
        }
        
        foreach ($this->join as &$join) {
            $sql .= $join[0]->_sqlOfOrder();
        }
        
        return rtrim($sql, ', ');
    }
    
    /**
     * Возвращает SQL для конструкции GROUP.
     *
     * @return string
     */
    public function _sqlOfGroup()
    {
        $sql = '';
        
        if(count($this->group))
        {
            $sql    = 'GROUP BY';
            foreach ($this->group as $field => $value)
            {
                $sql .= (strpos($field, '(') === false) ? " `$this->alias`.`$field`, " : " $field, " ;
            }
        }
        
        foreach ($this->join as &$join) {
            $sql .= $join[0]->_sqlOfGroup();
        }

        return rtrim($sql, ', ');
    }
    
    /**
     * Возвращает SQL для конструкции LIMIT.
     *
     * @return string
     */
    public function _sqlOfLimit()
    {
        if(sizeof($this->limit))
        {
            return "LIMIT " . (int) $this->limit[0] . ", " . (int) $this->limit[1];
        }
    }
    
    /**
     * Alias for table
     *
     * @param string $alias
     * @return DbQueryConstructor
     */
    public function alias($alias)
    {
        $this->alias = $alias;
        return $this;
    }
    
    /**
     * Добавить фильтр.
     *
     * @param string $field
     * @param mixed $value
     * @param enum $operator =,!=,<>,>,<,IN, NOT IN, BETWEEN, LIKE
     * @return DbQueryConstructor
     */
    public function filter($field, $value, $operator = false)
    {
        // Если значение массив, оператор равен "IN".
        $operator           = (!$operator && is_array($value)) ? 'IN' : (!$operator ? '=' : $operator);
        $this->filters[]    = array($field, $value, $operator);
        return $this;
    }
    
    /**
     * Добавить фильтр.
     *
     * @param string $field
     * @param mixed $value
     * @param enum $operator =,!=,<>,>,<,IN, NOT IN, BETWEEN, LIKE
     * @return DbQueryConstructor
     */
    public function filterGroup($fields, $value, $operator = false)
    {
        // Если значение массив, оператор равен "IN".
        $operator           = (!$operator && is_array($value)) ? 'IN' : (!$operator ? '=' : $operator);
        $this->groups[]    = array($fields, $value, $operator);
        return $this;
    }
    
    /**
     * Связать поля экземпляра класса с полем экземпляра класса.
     *
     * @param DbQueryConstructor $ref
     * @param string $refield
     * @param string $field
     * @param string $operator
     * @return DbQueryConstructor
     */
    public function bind(DbQueryConstructor $ref, $refield, $field, $operator = '=')
    {
        $this->binds[]  = array($ref, $refield, $field, $operator);
        return $this;
    }
    
    /**
     * Добавить BETWEEN фильтр.
     *
     * @param string $field
     * @param array $array
     * @return DbQueryConstructor
     */
    public function between($field, array $between)
    {
        return $this->filter($field, $between, 'BETWEEN');
    }
    
    /**
     * Собрать запрос, выполнить и вернуть результат.
     *
     * @return DbResultSelect
     */
    public function find()
    {
        $this->assembling();
        return $this->context->query($this->sql);
    }
    
    /**
     * Делает count для записей.
     *
     * @return int      число записей
     */
    public function count()
    {
        $this->assemblingCount();
        $result = $this->context->query($this->sql);
        return $result->fetchField('count', 0);
    }
    
    /**
     * Найти запись по ее идентификатору.
     *
     * @param int $value
     * @param string $field
     * @return DbResultSelect
     */
    function findById($value, $field = 'id')
    {
        $this->filter($field, $value);
        return $this->find();
    }
    
    /**
     * Присоединяет таблицу. 
     * Передаваемый экземпляр класса предоставляет доступ к 
     * SQL собранных фильтров.
     *
     * @param DbQueryConstructor $join
     * @param int $type
     * @return DbQueryConstructor
     */
    public function join(DbQueryConstructor $join, $type = self::INNER_JOIN)
    {
        $join->setParent($this);
        $this->join[]   = array($join, $type);
        return $this;
    }
    
    /**
     * Добавляет объявление используемых индексов для выборки 
     * текущей таблицы.
     *
     * @param string $name
     */
    public function useIndex($name)
    {
        $this->useIndex = '';
        
        if(!empty($name)) {
            $this->useIndex = "`" . $this->context->escape($name) . "`";
        }
    }
    
    /**
     * Существует ли объект верхнего уровня (т.е. фактически является проверкой
     * на JOIN текущего экземпляра-класса к конструктору).
     *
     * @return boolean
     */
    public function hasParent()
    {
        return ($this->parent instanceof DbQueryConstructor);
    }
    
    /**
     * Получает ссылку на объект конструктора верхнего уровня.
     *
     * @param DbQueryConstructor $parent
     */
    public function setParent(DbQueryConstructor $parent)
    {
        $this->parent   = $parent;
    }
    
    /**
     * Задает сортировку
     *
     * @param string $field
     * @param enum $direct [ASC, DESC]
     * @return DbQueryConstructor
     */
    public function order($field, $direct = 'ASC')
    {
        $this->order[$field]   = $direct;
        return $this;
    }
    
    /**
     * Задает группировку по полю
     *
     * @param string $field
     * @return DbQueryConstructor
     */
    public function group($field)
    {
        $this->group[$field] = 1;
        return $this;
    }
    
    /**
     * Устанавливает лимит на выборку.
     *
     * @param int $start
     * @param int $limit
     * @return DbQueryConstructor
     */
    public function limit($start, $limit)
    {
        $this->limit    = array($start, $limit);
        return $this;
    }
    
    /**
     * Очистка фильтров.
     *
     */
    public function clear()
    {
        // Чистим, моем, драем, трем,
        // Джоины свои до дыр протрем!
        foreach ($this->join as &$join)
        {
            $join->clear();
        }
        
        $this->join     = array();
        $this->filters  = array();
        $this->limit    = array();
        $this->order    = array();
        $this->binds    = array();
        $this->sql      = '';
        $this->parent   = null;
        $this->select   = array('*');
    }
}

?>